package io.gitee.minelx.filesystem.provider;

import io.gitee.minelx.filesystem.FileAccessingException;
import io.gitee.minelx.filesystem.values.PathDescription;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.List;
import java.util.Objects;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

class JavaFSPathMethodProvider implements PathMethodProvider {

    private final FileSystem fileSystem;

    private final String schemaAndAuthority;

    private final PathDescription description;

    private final Path path;

    JavaFSPathMethodProvider(FileSystem fileSystem, String schemaAndAuthority, PathDescription description) {
        this.fileSystem = fileSystem;
        this.schemaAndAuthority = schemaAndAuthority;
        this.description = description;
        // create path first
        path = createPath(fileSystem, description);
    }

    @Override
    public String uri() {
        return schemaAndAuthority + description.toText();
    }

    @Override
    public OutputStream openOutputStream(StandardOpenOption openOption) {
        try {
            return fileSystem.provider().newOutputStream(path, openOption);
        } catch (IOException e) {
            throw new FileAccessingException("error while obtaining stream.", e);
        }
    }

    @Override
    public InputStream openInputStream() {
        try {
            return fileSystem.provider().newInputStream(path);
        } catch (IOException e) {
            throw new FileAccessingException("error while obtaining stream.", e);
        }
    }

    @Override
    public void createFile() {
        if (path.toFile().exists()) {
            deleteFile();
        }
        try {
            OutputStream outputStream = fileSystem.provider().newOutputStream(path, StandardOpenOption.CREATE);
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            throw new FileAccessingException("error while creating file.", e);
        }
    }

    @Override
    public void deleteFile() {
        if (!path.toFile().exists()) {
            throw new FileAccessingException("path doesn't exists.");
        }
        try {
            fileSystem.provider().delete(path);
        } catch (IOException e) {
            throw new FileAccessingException("error while deleting file.", e);
        }
    }

    @Override
    public void createDirectory() {
        if (exists()) {
            deleteDirectory();
        }
        try {
            fileSystem.provider().createDirectory(path);
        } catch (IOException e) {
            throw new FileAccessingException("error while creating directory.", e);
        }
    }

    @Override
    public void deleteDirectory() {
        delete0(path.toFile());
    }

    @Override
    public boolean exists() {
        return path.toFile().exists();
    }

    private void delete0(File file) {
        // delete self if it's file
        if (file.isFile()) {
            doDelete(file);
            return;
        }
        // delete sub files first if it's a directory
        for (String childName : Objects.requireNonNull(file.list())) {
            delete0(new File(file.getAbsolutePath() + "/" + childName));
        }
        // delete itself.
        doDelete(file);
    }

    @Override
    public boolean isFile() {
        return path.toFile().isFile();
    }

    @Override
    public boolean isDirectory() {
        return path.toFile().isDirectory();
    }

    @Override
    public String[] listDir() {
        return path.toFile().list();
    }

    @Override
    public void rename(String name) {
        Path fileSystemPath = createPath(fileSystem, description.sibling(name));
        try {
            Files.move(path, fileSystemPath, REPLACE_EXISTING);
        } catch (IOException e) {
            throw new FileAccessingException("error while renaming file.", e);
        }
    }

    private static void doDelete(File file) {
        boolean delete = file.delete();
        if (!delete) {
            throw new FileAccessingException("file can't be deleted: " + file);
        }
    }

    private static Path createPath(FileSystem fileSystem, PathDescription pathDescription) {
        List<String> steps = pathDescription.steps();
        return nastyPathFromSteps(fileSystem, steps);
    }

    private static Path nastyPathFromSteps(FileSystem fileSystem, List<String> steps) {
        if (steps.size() == 1) {
            return onlyOneStep(fileSystem, steps);
        }
        // flatten the steps
        return multiSteps(fileSystem, steps);
    }

    private static Path multiSteps(FileSystem fileSystem, List<String> steps) {
        String instead = steps.get(0);
        try {
            return fileSystem.getPath("/" + instead, steps.subList(1, steps.size()).toArray(new String[0]));
        } catch (InvalidPathException e) {
            // in windows, there would be InvalidPathException thrown if disk label specified ('C:', 'D:', ...).
            // whatever it happens, removed the head delimiter of the path to sign the path as a label-specified one.
            if (e.getMessage().startsWith("Illegal char <:> at index 2:")) {
                return fileSystem.getPath(instead, steps.subList(1, steps.size()).toArray(new String[0]));
            }
            throw e;
        }
    }

    private static Path onlyOneStep(FileSystem fileSystem, List<String> steps) {
        String theFirstOne = steps.get(0);
        try {
            return fileSystem.getPath("/" + theFirstOne);
        } catch (InvalidPathException e) {
            // in windows, there would be InvalidPathException thrown if disk label specified ('C:', 'D:', ...).
            // whatever it happens, removed the head delimiter of the path to sign the path as a label-specified one.
            if (e.getMessage().startsWith("Illegal char <:> at index 2:")) {
                return fileSystem.getPath(theFirstOne);
            }
            throw e;
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        JavaFSPathMethodProvider that = (JavaFSPathMethodProvider) o;
        return fileSystem.equals(that.fileSystem) && path.equals(that.path) && description.equals(that.description);
    }

    @Override
    public int hashCode() {
        return Objects.hash(fileSystem, path, description);
    }
}